Skip to content

feat(analyzer): add PHP support to content extractor (closes #15)#67

Merged
smorchj merged 1 commit intomainfrom
feat/php-content-extractor
Apr 15, 2026
Merged

feat(analyzer): add PHP support to content extractor (closes #15)#67
smorchj merged 1 commit intomainfrom
feat/php-content-extractor

Conversation

@smorchj
Copy link
Copy Markdown
Owner

@smorchj smorchj commented Apr 15, 2026

Summary

Adds PHP (`.php`) support to the content extractor, mirroring the existing Python, Java, and Ruby blocks inside `extractExports()`.

Detects and emits `FileExport` entries for:

PHP pattern Kind
`function name(...)` `function` with parameter signature
`public function name(...)` `function` with parameter signature
`class Name` `class`
`interface Name` `interface`
`trait Name` `class` (no trait kind available)
`const NAME = ...` `const`

All names and signatures pass through `sanitizeExportName` / `sanitizeExportSignature` to maintain the same injection-hardening posture as the other extractors.

Tests

7 new tests in `content-extractor.test.ts` inside a `describe('extractDirectoryContent — PHP')` block:

  1. Top-level function with parameters
  2. Public class method
  3. Class extraction
  4. Interface extraction
  5. Trait → class kind mapping
  6. Constants
  7. Injection-flag case — a PHP function called `IgnoreAllPreviousInstructions` comes out as `[flagged]`

Test count: 71 → 78 (35 extractor tests total, was 28).

How this PR was produced

This PR was written by Klonode itself — opened by Claude running inside the Klonode Workstation chat panel, using the same UI flow a human would. The task description was pasted into the chat textarea, the send button was clicked, and Claude worked through the existing Python/Java/Ruby pattern as the template. Two real Klonode bugs were found along the way and are being fixed in a separate PR.

Closes

Closes #15.

Adds a PHP extractor block inside extractExports() mirroring the
existing Python, Java, and Ruby extractors. Detects top-level and
public class functions (with parameter signatures), classes,
interfaces, traits (mapped to 'class' kind), and constants. All
names and signatures pass through sanitizeExportName /
sanitizeExportSignature, matching the injection-hardening posture
of the other extractors.

Adds 7 new tests in content-extractor.test.ts covering each PHP
construct plus a IgnoreAllPreviousInstructions.php injection fixture
that must come out as [flagged].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
smorchj added a commit that referenced this pull request Apr 15, 2026
Found while dogfooding Klonode on itself: opening the Workstation UI,
typing any prompt in the chat panel, and clicking send failed
immediately with "Feil: get is not defined" and the request never
reached the /api/chat/stream endpoint. Two independent bugs chained
together to make the UI unusable out of the box.

## Bug 1: missing `get` import in agents.ts

The self-hosting-survival helpers getCliSessionId / setCliSessionId /
clearCliSessionId call svelte/store's `get(sessionsStore)` but the
file only imported `{ writable, derived }`. Every chat send threw
ReferenceError at JSON.stringify(body) time because the fetch body
built `sessionId: getCliSessionId(...)`. Caught by the catch block
and rendered as "Feil: get is not defined" in the assistant bubble,
with no fetch ever being made.

Fix: import { writable, derived, get } from 'svelte/store'.

## Bug 2: demo graph ships with a placeholder repoPath

static/demo-graph.json had `"repoPath": "/path/to/your/project"` —
a literal placeholder. The ChatPanel passes graphStore.repoPath as
the cwd to /api/chat/stream, which passes it to spawn() as the child
working directory. On a fresh install a first-time user's chat send
would try to spawn Claude CLI in a nonexistent directory.

Fix:
1. static/demo-graph.json now uses "" for repoPath so the intent is
   unambiguous.
2. /api/chat/stream now validates body.repoPath exists on disk and
   falls back to process.cwd() with a console.warn if it doesn't.
   That gives a sane default instead of an opaque spawn failure when
   the user hasn't configured their project yet.

## How these were found

Sent a short "Reply with just READY" test through the chat textarea
via the preview tools, got "Feil: get is not defined". Stepped
through the send path, monkey-patched window.fetch to see the body,
found the fetch was never called. Traced backwards from JSON.stringify
and found getCliSessionId → get() → missing import. After fixing that,
repeated the test, got a working response, then validated end-to-end
by having Claude inside the chat panel write PR #67 (PHP extractor
support). Both fixes verified by the full "type in textarea, click
send" human path.

## Test plan

- pnpm --filter @klonode/core test — 78/78 passing
- Manual: Klonode Workstation UI, fresh page load, type a short
  prompt, click send. Before: "Feil: get is not defined" in the
  assistant bubble within 100ms. After: streaming Claude CLI
  response with tool calls visible.
smorchj added a commit that referenced this pull request Apr 15, 2026
Two separate issues combined to make long coding tasks inside the
Workstation chat panel hit a dead end with the generic fallback message
\"Claude brukte alle steg. Prøv et mer spesifikt spørsmål.\"

## Issue 1: max-turns was 50 for bypass mode

A realistic coding task — \"add a language extractor and 5-9 unit tests,
run the suite, commit on a branch\" — routinely needs 80-150 tool calls.
Bypass mode was capped at 50, which hit the ceiling mid-task and caused
Claude to return an empty result with num_turns = 50. The UI then
rendered the fallback \"alle steg\" message, making the session appear
dead.

Raise bypass and CO to 500. question (1) and plan (15) stay tight
because those modes exist precisely to cap turn spend.

## Issue 2: -p mode session IDs are not reliably resumable

Klonode captured the session_id from Claude CLI's first `init` stream
event and stored it as the tab's `cliSessionIds` entry. On the next
send, ChatPanel re-included it as `--resume <id>` so Claude would pick
up the conversation.

Problem: a session spawned with `-p` emits that session_id but does
NOT stay in Claude CLI's resumable session cache the same way
interactive sessions do. The next `--resume` spawn returns:

    No conversation found with session ID: <uuid>

Claude then emits a result of type `error_during_execution` with empty
text, and the UI falls back to \"Claude brukte alle steg.\" The session
appears unresponsive forever until the user clicks + for a new tab.

Fix: drop `--resume` entirely from the stream endpoint. Every send is a
fresh spawn, always with the system prompt prepended. Continuity on
the user side lives in the persisted `chatStore.messages`, and Claude
re-routes against the generated CLAUDE.md + CONTEXT.md on every
message, which matches Klonode's per-query routing philosophy.

## How these were found

Ran `klonode init` on the repo, saw the full contextualized tree in
the Workstation TreeView, sent a realistic task (\"add Go extractor
support with 9 tests\") through the chat textarea + send button. Got
the \"alle steg\" fallback. Direct CLI test with the stored sessionId
reproduced the \"No conversation found\" error. Sent again without
--resume, hit a separate cap at exactly 50 tool calls. Raised to 500
and dropped --resume in one change.

After this fix, the same task completes in ~6 tool calls and produces
a branch + tests + passing suite.

## Sibling PRs

- #68 fixes the `get is not defined` crash + demo graph repoPath + auto
  loading the real project graph at boot
- #67 and #69 are the \"this PR was written by Klonode itself\" PRs
  (PHP and Go extractors) that these blockers were preventing
@smorchj smorchj merged commit 4100a4f into main Apr 15, 2026
1 check passed
smorchj added a commit that referenced this pull request Apr 15, 2026
…68)

* Fix two blockers that crashed every first-time chat send

Found while dogfooding Klonode on itself: opening the Workstation UI,
typing any prompt in the chat panel, and clicking send failed
immediately with "Feil: get is not defined" and the request never
reached the /api/chat/stream endpoint. Two independent bugs chained
together to make the UI unusable out of the box.

## Bug 1: missing `get` import in agents.ts

The self-hosting-survival helpers getCliSessionId / setCliSessionId /
clearCliSessionId call svelte/store's `get(sessionsStore)` but the
file only imported `{ writable, derived }`. Every chat send threw
ReferenceError at JSON.stringify(body) time because the fetch body
built `sessionId: getCliSessionId(...)`. Caught by the catch block
and rendered as "Feil: get is not defined" in the assistant bubble,
with no fetch ever being made.

Fix: import { writable, derived, get } from 'svelte/store'.

## Bug 2: demo graph ships with a placeholder repoPath

static/demo-graph.json had `"repoPath": "/path/to/your/project"` —
a literal placeholder. The ChatPanel passes graphStore.repoPath as
the cwd to /api/chat/stream, which passes it to spawn() as the child
working directory. On a fresh install a first-time user's chat send
would try to spawn Claude CLI in a nonexistent directory.

Fix:
1. static/demo-graph.json now uses "" for repoPath so the intent is
   unambiguous.
2. /api/chat/stream now validates body.repoPath exists on disk and
   falls back to process.cwd() with a console.warn if it doesn't.
   That gives a sane default instead of an opaque spawn failure when
   the user hasn't configured their project yet.

## How these were found

Sent a short "Reply with just READY" test through the chat textarea
via the preview tools, got "Feil: get is not defined". Stepped
through the send path, monkey-patched window.fetch to see the body,
found the fetch was never called. Traced backwards from JSON.stringify
and found getCliSessionId → get() → missing import. After fixing that,
repeated the test, got a working response, then validated end-to-end
by having Claude inside the chat panel write PR #67 (PHP extractor
support). Both fixes verified by the full "type in textarea, click
send" human path.

## Test plan

- pnpm --filter @klonode/core test — 78/78 passing
- Manual: Klonode Workstation UI, fresh page load, type a short
  prompt, click send. Before: "Feil: get is not defined" in the
  assistant bubble within 100ms. After: streaming Claude CLI
  response with tool calls visible.

* Auto-load the real project graph at boot instead of the demo fixture

Third blocker found while dogfooding: even after fixing the chat send
path, the TreeView and GraphView still showed a fake `demo-project`
tree with `app / lib / tests` folders. The Workstation hard-coded
\`loadGraphFromUrl('/demo-graph.json')\` in +page.svelte onMount, so
every user — regardless of which real repo they had Klonode running
in — saw the same bundled fixture.

## Fix

- **New endpoint** GET /api/graph/current. Walks up from the server's
  cwd looking for \`.klonode/graph.json\` and returns it if found.
  Responds 404 when no initialized project is found so the client can
  fall back. Also supports ?repoPath=<abs> for explicit targeting.
- **New loader helper** loadGraphForCurrentProject() in loader.ts.
  Tries /api/graph/current first, falls back to the bundled
  /demo-graph.json if the project isn't initialized.
- **+page.svelte** now calls loadGraphForCurrentProject() at boot.
  Tracks whether it loaded 'real' vs 'demo' so the UI can signal this
  in a future toolbar indicator.

## The walk-up matters

The Workstation dev server is typically started inside packages/ui/
(where launch.json's cwd points), not the repo root. A naive
\`process.cwd() + .klonode/graph.json\` check would 404 on every
monorepo. The new endpoint walks up parent directories (with a guard
against infinite loops) until it finds an initialized project or
hits the filesystem root.

## Verified

Dev server up, page loaded. Store now reports:
- rootName: "KlonodeV2" (was "demo-project")
- nodeCount: 69 (was 4)
- topLevelNames: [".github", ".marketing", "packages"]
  (was ["app", "lib", "tests"])

TreeView renders .github/ISSUE_TEMPLATE, .github/workflows,
.marketing, and packages/{cli, core, desktop, ui}.
smorchj added a commit that referenced this pull request Apr 15, 2026
Two separate issues combined to make long coding tasks inside the
Workstation chat panel hit a dead end with the generic fallback message
\"Claude brukte alle steg. Prøv et mer spesifikt spørsmål.\"

## Issue 1: max-turns was 50 for bypass mode

A realistic coding task — \"add a language extractor and 5-9 unit tests,
run the suite, commit on a branch\" — routinely needs 80-150 tool calls.
Bypass mode was capped at 50, which hit the ceiling mid-task and caused
Claude to return an empty result with num_turns = 50. The UI then
rendered the fallback \"alle steg\" message, making the session appear
dead.

Raise bypass and CO to 500. question (1) and plan (15) stay tight
because those modes exist precisely to cap turn spend.

## Issue 2: -p mode session IDs are not reliably resumable

Klonode captured the session_id from Claude CLI's first `init` stream
event and stored it as the tab's `cliSessionIds` entry. On the next
send, ChatPanel re-included it as `--resume <id>` so Claude would pick
up the conversation.

Problem: a session spawned with `-p` emits that session_id but does
NOT stay in Claude CLI's resumable session cache the same way
interactive sessions do. The next `--resume` spawn returns:

    No conversation found with session ID: <uuid>

Claude then emits a result of type `error_during_execution` with empty
text, and the UI falls back to \"Claude brukte alle steg.\" The session
appears unresponsive forever until the user clicks + for a new tab.

Fix: drop `--resume` entirely from the stream endpoint. Every send is a
fresh spawn, always with the system prompt prepended. Continuity on
the user side lives in the persisted `chatStore.messages`, and Claude
re-routes against the generated CLAUDE.md + CONTEXT.md on every
message, which matches Klonode's per-query routing philosophy.

## How these were found

Ran `klonode init` on the repo, saw the full contextualized tree in
the Workstation TreeView, sent a realistic task (\"add Go extractor
support with 9 tests\") through the chat textarea + send button. Got
the \"alle steg\" fallback. Direct CLI test with the stored sessionId
reproduced the \"No conversation found\" error. Sent again without
--resume, hit a separate cap at exactly 50 tool calls. Raised to 500
and dropped --resume in one change.

After this fix, the same task completes in ~6 tool calls and produces
a branch + tests + passing suite.

## Sibling PRs

- #68 fixes the `get is not defined` crash + demo graph repoPath + auto
  loading the real project graph at boot
- #67 and #69 are the \"this PR was written by Klonode itself\" PRs
  (PHP and Go extractors) that these blockers were preventing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add PHP support to content extractor

1 participant