@projectctx/agent 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,11 +13,38 @@ security expectations.
13
13
  ```bash
14
14
  npm install -g @projectctx/agent@alpha
15
15
  projectctx login --token <agent-token>
16
+ projectctx connect --dry-run # preview client wiring; drop --dry-run to apply
16
17
  projectctx doctor
17
18
  projectctx scan .
18
19
  projectctx mcp
19
20
  ```
20
21
 
22
+ ## `projectctx connect`
23
+
24
+ One command wires the `project-memory` MCP into every locally installed AI
25
+ client and installs the memory instruction blocks:
26
+
27
+ - **Detects** Claude Code (`~/.claude.json`), Cursor (`~/.cursor/mcp.json`), and
28
+ Codex (`~/.codex/config.toml`) by config presence.
29
+ - **Wires the MCP** per client — `claude mcp add …` when the `claude` CLI is on
30
+ PATH, otherwise a direct JSON merge for Claude Code; JSON merge for Cursor;
31
+ TOML block append for Codex.
32
+ - **Installs instruction blocks** (from `assets/instructions/`) into
33
+ `~/.claude/CLAUDE.md`, `~/.cursor/rules/projectctx.mdc`, and
34
+ `~/.codex/AGENTS.md`, wrapped in `<!-- projectctx:begin/end -->` markers so
35
+ re-runs replace the block instead of appending.
36
+ - **Prints the manual remainder** for ChatGPT / claude.ai / Claude Desktop
37
+ (account-side connectors — no local config file).
38
+
39
+ Safety: every config file is parsed → merged → written atomically (temp +
40
+ rename) with a timestamped `.bak-<ts>` backup, and connect warns if the target
41
+ client appears to be running. `--dry-run` prints the full plan without touching
42
+ disk. Re-runs are idempotent (empty diff on already-wired files).
43
+
44
+ Instruction texts ship in two versions; `v1` (goal-mode) is installed today.
45
+ Swap to `v2` (checkin-mode) by re-running `connect` after roadmap T-301 — see
46
+ [assets/instructions/README.md](assets/instructions/README.md).
47
+
21
48
  PR 20 publishes public alpha packages only after local pack verification passes.
22
49
  Use the `alpha` dist-tag, never `latest`.
23
50
 
@@ -0,0 +1,35 @@
1
+ # ProjectCtx instruction texts (T-002)
2
+
3
+ These are the bootstrap instruction blocks that `projectctx connect` (T-001)
4
+ installs into each client's rules file. They are the entire trigger mechanism on
5
+ clients without hooks, and the discretionary half (what to search, what to save)
6
+ even where hooks exist. See `roadmap/tasks/T-002-bootstrap-instructions.md`.
7
+
8
+ ## Versioning (load-bearing)
9
+
10
+ - **v1** is the installed default today. It uses **goal-mode** wording
11
+ (`get_context` with the session's goal) because `get_context {mode:"checkin"}`
12
+ FAILS Zod validation until T-301 deploys (`goal` is currently required).
13
+ - **v2** uses **checkin-mode** wording. Swap it in by re-running
14
+ `projectctx connect` *after T-301 ships*. The `<!-- projectctx:begin/end -->`
15
+ markers make the swap a clean replace, not an append.
16
+
17
+ Never ship both versions into one file — the two first sentences contradict.
18
+
19
+ To move the default from v1 to v2, change `INSTRUCTION_VERSION` in
20
+ `src/connect.ts` (currently `"v1"`).
21
+
22
+ ## Files
23
+
24
+ | Client | File(s) | Installed into |
25
+ |--------|---------|----------------|
26
+ | Claude Code | `claude-code.v1.md` / `.v2.md` | `~/.claude/CLAUDE.md` |
27
+ | Cursor | `cursor.v1.md` / `.v2.md` | `~/.cursor/rules/projectctx.mdc` (with frontmatter) |
28
+ | Codex | `codex.v1.md` / `.v2.md` | `~/.codex/AGENTS.md` |
29
+ | ChatGPT / claude.ai | `chatgpt-claudeai.v1.md` / `.v2.md` | pasted manually (custom instructions) |
30
+
31
+ The Cursor `.mdc` frontmatter (`description` + `alwaysApply: true`) is written by
32
+ `connect` outside the marker block, since frontmatter must sit at the file top.
33
+
34
+ The ChatGPT / claude.ai text is printed as part of the manual remainder — those
35
+ connectors are account-side (settings UIs), not local config files.
@@ -0,0 +1,6 @@
1
+ I use a personal memory service (project-memory connector). At the start of
2
+ a conversation, call get_context with this conversation's goal and use it as
3
+ background about me. Search it whenever a topic from its memory map comes up or I
4
+ reference past work. Save important decisions and learnings before we finish;
5
+ update existing records rather than duplicating. Don't save trivia. If asked
6
+ what you remember about me, answer from get_context and searches — don't guess.
@@ -0,0 +1,6 @@
1
+ I use a personal memory service (project-memory connector). At the start of
2
+ a conversation, call get_context with mode "checkin" and use it as background
3
+ about me. Search it whenever a topic from its memory map comes up or I
4
+ reference past work. Save important decisions and learnings before we finish;
5
+ update existing records rather than duplicating. Don't save trivia. If asked
6
+ what you remember about me, answer from checkin and searches — don't guess.
@@ -0,0 +1,15 @@
1
+ ## Memory (project-memory MCP)
2
+
3
+ At session start, call get_context on project-memory with this session's goal
4
+ and treat the result as background truth about me and my work.
5
+
6
+ During the session, search memory before assuming or asking when a topic
7
+ matching any collection in the returned memory map comes up, or when I
8
+ reference past work, a decision, or "what we discussed" — and before saying
9
+ something doesn't exist.
10
+
11
+ Before the session ends — or right after any significant decision — save
12
+ durable outcomes: decisions and why, learnings that outlive this session,
13
+ corrections I gave you. Prefer updating existing records (via
14
+ preview_memory_changes upserts) over creating new ones (save_memory).
15
+ Never save session-local trivia.
@@ -0,0 +1,15 @@
1
+ ## Memory (project-memory MCP)
2
+
3
+ At session start, call get_context with {mode: "checkin"} once and treat
4
+ the result as background truth about me and my work.
5
+
6
+ During the session, search memory before assuming or asking when a topic
7
+ matching any collection in the checkin map comes up, or when I reference
8
+ past work, a decision, or "what we discussed" — and before saying
9
+ something doesn't exist.
10
+
11
+ Before the session ends — or right after any significant decision — save
12
+ durable outcomes: decisions and why, learnings that outlive this session,
13
+ corrections I gave you. Prefer updating existing records (via
14
+ preview_memory_changes upserts) over creating new ones (save_memory).
15
+ Never save session-local trivia.
@@ -0,0 +1,10 @@
1
+ ## Memory (project-memory MCP)
2
+
3
+ At session start, call get_context on project-memory with this session's goal;
4
+ treat the result as background truth about me and my work. Search memory before
5
+ assuming or asking when a topic from the returned memory map comes up, when I
6
+ reference past work or a decision, or before saying something doesn't exist.
7
+ Before finishing — or right after a significant decision — save durable outcomes
8
+ (decisions and why, lasting learnings, corrections I gave). Prefer updating
9
+ existing records (preview_memory_changes upserts) over new ones (save_memory).
10
+ Never save session-local trivia.
@@ -0,0 +1,10 @@
1
+ ## Memory (project-memory MCP)
2
+
3
+ At session start, call get_context with {mode: "checkin"} once; treat the result
4
+ as background truth about me and my work. Search memory before assuming or asking
5
+ when a topic from the checkin map comes up, when I reference past work or a
6
+ decision, or before saying something doesn't exist. Before finishing — or right
7
+ after a significant decision — save durable outcomes (decisions and why, lasting
8
+ learnings, corrections I gave). Prefer updating existing records
9
+ (preview_memory_changes upserts) over new ones (save_memory). Never save
10
+ session-local trivia.
@@ -0,0 +1,10 @@
1
+ ## Memory (project-memory MCP)
2
+
3
+ At session start, call get_context on project-memory with this session's goal;
4
+ treat the result as background truth about me and my work. Search memory before
5
+ assuming or asking when a topic from the returned memory map comes up, when I
6
+ reference past work or a decision, or before saying something doesn't exist.
7
+ Before finishing — or right after a significant decision — save durable outcomes
8
+ (decisions and why, lasting learnings, corrections I gave). Prefer updating
9
+ existing records (preview_memory_changes upserts) over new ones (save_memory).
10
+ Never save session-local trivia.
@@ -0,0 +1,10 @@
1
+ ## Memory (project-memory MCP)
2
+
3
+ At session start, call get_context with {mode: "checkin"} once; treat the result
4
+ as background truth about me and my work. Search memory before assuming or asking
5
+ when a topic from the checkin map comes up, when I reference past work or a
6
+ decision, or before saying something doesn't exist. Before finishing — or right
7
+ after a significant decision — save durable outcomes (decisions and why, lasting
8
+ learnings, corrections I gave). Prefer updating existing records
9
+ (preview_memory_changes upserts) over new ones (save_memory). Never save
10
+ session-local trivia.
package/dist/cli.d.ts CHANGED
@@ -6,6 +6,10 @@ export type CliCommand = {
6
6
  } | {
7
7
  command: "doctor";
8
8
  debug: boolean;
9
+ } | {
10
+ command: "connect";
11
+ debug: boolean;
12
+ dryRun: boolean;
9
13
  } | {
10
14
  command: "scan";
11
15
  debug: boolean;
@@ -30,6 +34,29 @@ export type CliCommand = {
30
34
  command: "workspace-use";
31
35
  debug: boolean;
32
36
  workspaceId: string;
37
+ } | {
38
+ command: "token-create";
39
+ debug: boolean;
40
+ scopes: string[];
41
+ name?: string;
42
+ authToken?: string;
43
+ expiresInSeconds?: number;
44
+ workspaceId?: string;
45
+ apiUrl?: string;
46
+ noStore: boolean;
47
+ } | {
48
+ command: "token-list";
49
+ debug: boolean;
50
+ authToken?: string;
51
+ workspaceId?: string;
52
+ apiUrl?: string;
53
+ } | {
54
+ command: "token-revoke";
55
+ debug: boolean;
56
+ credentialId: string;
57
+ authToken?: string;
58
+ workspaceId?: string;
59
+ apiUrl?: string;
33
60
  };
34
61
  export declare function parseCliArgs(argv: string[]): CliCommand;
35
62
  export declare function runCli(argv?: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -6,6 +6,8 @@ import { runDoctor } from "./doctor.js";
6
6
  import { runScanWorkspace } from "./scan-workspace.js";
7
7
  import { startMcpServer } from "./mcp.js";
8
8
  import { runLogin, runLogout, runWhoami, runWorkspaceUse, runWorkspaces } from "./auth-commands.js";
9
+ import { runConnect, formatConnectOutput } from "./connect.js";
10
+ import { runTokenCreate, runTokenList, runTokenRevoke } from "./token-commands.js";
9
11
  import { AgentError, toErrorShape } from "./errors.js";
10
12
  function usage(exitCode = 1) {
11
13
  process.stderr.write([
@@ -14,12 +16,16 @@ function usage(exitCode = 1) {
14
16
  "Commands:",
15
17
  " projectctx mcp [--debug]",
16
18
  " projectctx doctor [--debug]",
19
+ " projectctx connect [--dry-run] [--debug]",
17
20
  " projectctx scan [rootPath] [options]",
18
21
  " projectctx login [--token <token>] [--api-url <url>] [--allow-file-token-fallback] [--debug]",
19
22
  " projectctx logout [--debug]",
20
23
  " projectctx whoami [--debug]",
21
24
  " projectctx workspaces [--debug]",
22
25
  " projectctx workspace use <workspaceId> [--debug]",
26
+ " projectctx token create --scopes <a,b> [--name <n>] [--auth-token <t>] [--expires-in-days <n>] [--workspace <id>] [--no-store] [--debug]",
27
+ " projectctx token list [--auth-token <t>] [--workspace <id>] [--debug]",
28
+ " projectctx token revoke <credentialId> [--auth-token <t>] [--workspace <id>] [--debug]",
23
29
  "",
24
30
  "Scan options:",
25
31
  " --max-repos <n>",
@@ -56,6 +62,19 @@ export function parseCliArgs(argv) {
56
62
  }
57
63
  return { command, debug };
58
64
  }
65
+ if (command === "connect") {
66
+ let debug = false;
67
+ let dryRun = false;
68
+ for (const arg of rest) {
69
+ if (arg === "--debug")
70
+ debug = true;
71
+ else if (arg === "--dry-run")
72
+ dryRun = true;
73
+ else
74
+ usage();
75
+ }
76
+ return { command: "connect", debug, dryRun };
77
+ }
59
78
  if (command === "login") {
60
79
  let debug = false;
61
80
  let token;
@@ -82,6 +101,74 @@ export function parseCliArgs(argv) {
82
101
  }
83
102
  return { command: "login", debug, token, apiUrl, allowFileTokenFallback };
84
103
  }
104
+ if (command === "token") {
105
+ const [sub, ...flags] = rest;
106
+ if (sub !== "create" && sub !== "list" && sub !== "revoke")
107
+ usage();
108
+ let debug = false;
109
+ let authToken;
110
+ let workspaceId;
111
+ let apiUrl;
112
+ let name;
113
+ let scopes = [];
114
+ let expiresInSeconds;
115
+ let noStore = false;
116
+ let credentialId;
117
+ for (let i = 0; i < flags.length; i++) {
118
+ const arg = flags[i];
119
+ if (arg === "--debug")
120
+ debug = true;
121
+ else if (arg === "--no-store")
122
+ noStore = true;
123
+ else if (arg === "--auth-token") {
124
+ if (!flags[i + 1])
125
+ usage();
126
+ authToken = flags[++i];
127
+ }
128
+ else if (arg === "--workspace") {
129
+ if (!flags[i + 1])
130
+ usage();
131
+ workspaceId = flags[++i];
132
+ }
133
+ else if (arg === "--api-url") {
134
+ if (!flags[i + 1])
135
+ usage();
136
+ apiUrl = flags[++i];
137
+ }
138
+ else if (arg === "--name") {
139
+ if (!flags[i + 1])
140
+ usage();
141
+ name = flags[++i];
142
+ }
143
+ else if (arg === "--scopes" || arg === "--scope") {
144
+ if (!flags[i + 1])
145
+ usage();
146
+ // Accept comma-separated or repeated flags.
147
+ for (const value of flags[++i].split(",")) {
148
+ const trimmed = value.trim();
149
+ if (trimmed)
150
+ scopes.push(trimmed);
151
+ }
152
+ }
153
+ else if (arg === "--expires-in-days") {
154
+ if (!flags[i + 1])
155
+ usage();
156
+ expiresInSeconds = readPositiveInt(flags[++i], arg) * 24 * 60 * 60;
157
+ }
158
+ else if (!arg.startsWith("-") && sub === "revoke" && !credentialId) {
159
+ credentialId = arg;
160
+ }
161
+ else
162
+ usage();
163
+ }
164
+ if (sub === "create")
165
+ return { command: "token-create", debug, scopes, name, authToken, expiresInSeconds, workspaceId, apiUrl, noStore };
166
+ if (sub === "list")
167
+ return { command: "token-list", debug, authToken, workspaceId, apiUrl };
168
+ if (!credentialId)
169
+ usage();
170
+ return { command: "token-revoke", debug, credentialId, authToken, workspaceId, apiUrl };
171
+ }
85
172
  if (command === "workspace") {
86
173
  const [sub, workspaceId, ...flags] = rest;
87
174
  if (sub !== "use" || !workspaceId || workspaceId.startsWith("-"))
@@ -153,6 +240,14 @@ export async function runCli(argv = process.argv.slice(2)) {
153
240
  case "doctor":
154
241
  printJson(await runDoctor(logger));
155
242
  return;
243
+ case "connect": {
244
+ // Onboarding copy is human-readable, not JSON. The real run shells out
245
+ // to `claude mcp add` when available (runCliCommands: true); --dry-run
246
+ // never writes or shells out.
247
+ const result = runConnect({ dryRun: parsed.dryRun, runCliCommands: !parsed.dryRun, logger });
248
+ process.stdout.write(formatConnectOutput(result));
249
+ return;
250
+ }
156
251
  case "login":
157
252
  printJson(await runLogin({ token: parsed.token, apiUrl: parsed.apiUrl, allowFileTokenFallback: parsed.allowFileTokenFallback }, logger));
158
253
  return;
@@ -168,6 +263,28 @@ export async function runCli(argv = process.argv.slice(2)) {
168
263
  case "workspace-use":
169
264
  printJson(await runWorkspaceUse(parsed.workspaceId, logger));
170
265
  return;
266
+ case "token-create":
267
+ printJson(await runTokenCreate({
268
+ scopes: parsed.scopes,
269
+ name: parsed.name,
270
+ authToken: parsed.authToken,
271
+ expiresInSeconds: parsed.expiresInSeconds,
272
+ workspaceId: parsed.workspaceId,
273
+ apiUrl: parsed.apiUrl,
274
+ store: !parsed.noStore
275
+ }, logger));
276
+ return;
277
+ case "token-list":
278
+ printJson(await runTokenList({ authToken: parsed.authToken, workspaceId: parsed.workspaceId, apiUrl: parsed.apiUrl }, logger));
279
+ return;
280
+ case "token-revoke":
281
+ printJson(await runTokenRevoke({
282
+ credentialId: parsed.credentialId,
283
+ authToken: parsed.authToken,
284
+ workspaceId: parsed.workspaceId,
285
+ apiUrl: parsed.apiUrl
286
+ }, logger));
287
+ return;
171
288
  case "scan":
172
289
  printJson(runScanWorkspace({ rootPath: parsed.rootPath, ...parsed.options }, logger));
173
290
  return;
@@ -0,0 +1,101 @@
1
+ import type { DebugLogger } from "./log.js";
2
+ export declare const MCP_SERVER_NAME = "project-memory";
3
+ export declare const MCP_URL = "https://projectctx.com/mcp";
4
+ export declare const INSTRUCTION_VERSION: "v1" | "v2";
5
+ export type ClientId = "claude-code" | "cursor" | "codex";
6
+ export interface FileChange {
7
+ path: string;
8
+ kind: "mcp-config" | "instructions";
9
+ before: string | null;
10
+ after: string;
11
+ changed: boolean;
12
+ note?: string;
13
+ }
14
+ export interface ClientPlan {
15
+ client: ClientId;
16
+ label: string;
17
+ detected: boolean;
18
+ cliCommand?: {
19
+ argv: string[];
20
+ description: string;
21
+ };
22
+ changes: FileChange[];
23
+ warnings: string[];
24
+ }
25
+ export interface ConnectPlan {
26
+ dryRun: boolean;
27
+ home: string;
28
+ instructionVersion: "v1" | "v2";
29
+ clients: ClientPlan[];
30
+ manualRemainder: string;
31
+ }
32
+ export interface ConnectResult extends ConnectPlan {
33
+ applied: boolean;
34
+ }
35
+ interface ConnectPaths {
36
+ home: string;
37
+ claudeJson: string;
38
+ claudeMd: string;
39
+ cursorMcp: string;
40
+ cursorRules: string;
41
+ codexToml: string;
42
+ codexAgents: string;
43
+ }
44
+ export declare function connectPaths(home?: string): ConnectPaths;
45
+ export declare function readManualRemainderText(version: "v1" | "v2"): string;
46
+ /**
47
+ * Insert or replace the projectctx marker block in a markdown-ish file. Content
48
+ * outside the markers is preserved verbatim. Re-running with the same body is a
49
+ * no-op (byte-identical output), which is what makes connect idempotent.
50
+ */
51
+ export declare function upsertMarkerBlock(existing: string | null, body: string): string;
52
+ export declare function buildCursorMdc(existing: string | null, body: string): string;
53
+ interface JsonMergeResult {
54
+ after: string;
55
+ changed: boolean;
56
+ }
57
+ export declare function mergeMcpServersJson(existing: string | null, entry: Record<string, unknown>): JsonMergeResult;
58
+ export declare function upsertCodexToml(existing: string | null): JsonMergeResult;
59
+ /**
60
+ * Write `content` to `path` atomically: back up any existing file to
61
+ * `<target>.bak-<timestamp>`, write to a temp file in the same directory, then
62
+ * rename over the target (atomic on POSIX). The temp+rename means a crash
63
+ * mid-write never leaves a truncated config.
64
+ *
65
+ * Symlinked configs (dotfiles setups) are followed: the write and backup act on
66
+ * the resolved real path, so the link stays intact and the linked target gets
67
+ * the new content. Renaming over the link path itself would silently replace
68
+ * the symlink with a regular file and leave the real target stale (review
69
+ * finding S1).
70
+ */
71
+ export declare function atomicWriteWithBackup(path: string, content: string): {
72
+ backupPath?: string;
73
+ };
74
+ export declare function buildConnectPlan(opts?: {
75
+ home?: string;
76
+ version?: "v1" | "v2";
77
+ logger?: DebugLogger;
78
+ }): ConnectPlan;
79
+ export interface RunConnectOptions {
80
+ dryRun?: boolean;
81
+ home?: string;
82
+ version?: "v1" | "v2";
83
+ runCliCommands?: boolean;
84
+ logger?: DebugLogger;
85
+ }
86
+ export declare function runConnect(opts?: RunConnectOptions): ConnectResult;
87
+ export interface ClientConfigStatus {
88
+ client: ClientId;
89
+ label: string;
90
+ configPath: string;
91
+ configExists: boolean;
92
+ mcpConfigured: boolean;
93
+ }
94
+ export declare function inspectClientConfigs(home?: string): ClientConfigStatus[];
95
+ /**
96
+ * Render the plan as product-grade terminal copy. Doubles as onboarding, so it
97
+ * names every file it would touch, flags detected-vs-not, and prints the manual
98
+ * remainder. In dry-run it says nothing was written.
99
+ */
100
+ export declare function formatConnectOutput(result: ConnectResult): string;
101
+ export {};