@agentprojectcontext/apx 1.16.0 → 1.18.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -60,7 +60,8 @@
60
60
  "semver": "^7.8.0",
61
61
  "solid-js": "^1.9.12",
62
62
  "strip-ansi": "^7.2.0",
63
- "yargs": "^18.0.0"
63
+ "yargs": "^18.0.0",
64
+ "zod": "^3.25.76"
64
65
  },
65
66
  "optionalDependencies": {
66
67
  "fast-glob": "^3.3.2",
@@ -37,7 +37,7 @@ const ACK_ONLY_TOOLS = new Set(["send_telegram"]);
37
37
  // (the model already had its chance to call a real tool).
38
38
  const MAX_CONSECUTIVE_ACKS = 2;
39
39
 
40
- const DEFAULT_SYSTEM = `# Identity (override everything else)
40
+ export const DEFAULT_SYSTEM = `# Identity (override everything else)
41
41
  You are **APX** — Manuel's personal assistant running on his Mac.
42
42
  You are NOT a code analyzer, NOT a generic chatbot, NOT a tutor.
43
43
  You are an **action agent**: you USE TOOLS to do real things on Manuel's system.
@@ -4,6 +4,7 @@ import { onCleanup } from "solid-js"
4
4
  import fs from "node:fs"
5
5
  import os from "node:os"
6
6
  import path from "node:path"
7
+ import { spawn } from "node:child_process"
7
8
 
8
9
  const TOKEN_PATH = path.join(os.homedir(), ".apx", "daemon.token")
9
10
 
@@ -20,6 +21,9 @@ export type ApxEvent =
20
21
  | { type: "chunk"; sessionID: string; chunk: string }
21
22
  | { type: "final"; sessionID: string; text: string; usage?: { input_tokens: number; output_tokens: number } }
22
23
  | { type: "error"; sessionID: string; error: string }
24
+ | { type: "shell.start"; sessionID: string; shellID: string; command: string; cwd: string }
25
+ | { type: "shell.output"; sessionID: string; shellID: string; stream: "stdout" | "stderr"; chunk: string }
26
+ | { type: "shell.done"; sessionID: string; shellID: string; exitCode: number | null; signal: NodeJS.Signals | null }
23
27
 
24
28
  export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
25
29
  name: "SDK",
@@ -102,6 +106,27 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
102
106
  return (data as any).id as string
103
107
  }
104
108
 
109
+ function runShell(sessionID: string, command: string, cwd: string = process.cwd()): Promise<{ shellID: string; exitCode: number | null }> {
110
+ const shellID = `sh-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
111
+ emitter.emit("event", { type: "shell.start", sessionID, shellID, command, cwd })
112
+ return new Promise((resolve) => {
113
+ const child = spawn(command, { shell: true, cwd, env: process.env })
114
+ child.stdout?.on("data", (buf) => {
115
+ emitter.emit("event", { type: "shell.output", sessionID, shellID, stream: "stdout", chunk: buf.toString() })
116
+ })
117
+ child.stderr?.on("data", (buf) => {
118
+ emitter.emit("event", { type: "shell.output", sessionID, shellID, stream: "stderr", chunk: buf.toString() })
119
+ })
120
+ child.on("error", (err) => {
121
+ emitter.emit("event", { type: "shell.output", sessionID, shellID, stream: "stderr", chunk: `[spawn error] ${err.message}\n` })
122
+ })
123
+ child.on("close", (code, signal) => {
124
+ emitter.emit("event", { type: "shell.done", sessionID, shellID, exitCode: code, signal })
125
+ resolve({ shellID, exitCode: code })
126
+ })
127
+ })
128
+ }
129
+
105
130
  async function listSessions(): Promise<Array<{ id: string; title: string; updatedAt?: number }>> {
106
131
  try {
107
132
  const token = readToken()
@@ -146,7 +171,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
146
171
  fork: async (_opts: any) => ({ data: undefined, error: new Error("not supported") }),
147
172
  abort: async (_opts: any) => {},
148
173
  prompt: async (_opts: any) => {},
149
- shell: async (_opts: any) => {},
174
+ shell: async (opts: { sessionID?: string; command?: string; cwd?: string }) => {
175
+ if (!opts?.command) return { data: undefined }
176
+ const sid = opts.sessionID || (await createSession())
177
+ const r = await runShell(sid, opts.command, opts.cwd)
178
+ return { data: r }
179
+ },
150
180
  command: async (_opts: any) => {},
151
181
  refresh: async () => {},
152
182
  update: async (_opts: any) => ({ data: undefined }),
@@ -180,6 +210,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
180
210
  streamChat,
181
211
  createSession,
182
212
  listSessions,
213
+ runShell,
183
214
  }
184
215
  },
185
216
  })
@@ -13,10 +13,15 @@ export type ApxSession = {
13
13
  export type ApxMessage = {
14
14
  id: string
15
15
  sessionID: string
16
- role: "user" | "assistant"
16
+ role: "user" | "assistant" | "shell"
17
17
  text: string
18
18
  streaming?: boolean
19
19
  error?: boolean
20
+ // Shell-specific
21
+ shellID?: string
22
+ command?: string
23
+ cwd?: string
24
+ exitCode?: number | null
20
25
  }
21
26
 
22
27
  export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContext({
@@ -78,6 +83,55 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
78
83
  setStore("previousMessages", (prev) => [...prev, { role: "assistant", content: e.text }])
79
84
  }
80
85
 
86
+ if (ev.type === "shell.start") {
87
+ const e = ev
88
+ setStore(
89
+ "messages",
90
+ produce((draft) => {
91
+ ;(draft[e.sessionID] ??= []).push({
92
+ id: e.shellID,
93
+ sessionID: e.sessionID,
94
+ role: "shell",
95
+ text: "",
96
+ streaming: true,
97
+ shellID: e.shellID,
98
+ command: e.command,
99
+ cwd: e.cwd,
100
+ })
101
+ }),
102
+ )
103
+ }
104
+
105
+ if (ev.type === "shell.output") {
106
+ const e = ev
107
+ setStore(
108
+ "messages",
109
+ produce((draft) => {
110
+ const msgs = draft[e.sessionID]
111
+ if (!msgs) return
112
+ const target = msgs.find((m) => m.role === "shell" && m.shellID === e.shellID)
113
+ if (target) target.text += e.chunk
114
+ }),
115
+ )
116
+ }
117
+
118
+ if (ev.type === "shell.done") {
119
+ const e = ev
120
+ setStore(
121
+ "messages",
122
+ produce((draft) => {
123
+ const msgs = draft[e.sessionID]
124
+ if (!msgs) return
125
+ const target = msgs.find((m) => m.role === "shell" && m.shellID === e.shellID)
126
+ if (target) {
127
+ target.streaming = false
128
+ target.exitCode = e.exitCode
129
+ if (e.signal) target.text += `\n[killed by signal ${e.signal}]`
130
+ }
131
+ }),
132
+ )
133
+ }
134
+
81
135
  if (ev.type === "error") {
82
136
  const e = ev
83
137
  setStore(
@@ -121,6 +175,11 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
121
175
  return id
122
176
  }
123
177
 
178
+ async function runShell(command: string, cwd?: string) {
179
+ const sessionID = await ensureSession()
180
+ await sdk.runShell(sessionID, command, cwd ?? process.cwd())
181
+ }
182
+
124
183
  async function sendMessage(text: string) {
125
184
  const sessionID = await ensureSession()
126
185
  const userMsg: ApxMessage = {
@@ -172,6 +231,7 @@ export const { use: useApxSync, provider: ApxSyncProvider } = createSimpleContex
172
231
  async refresh() {},
173
232
  },
174
233
  sendMessage,
234
+ runShell,
175
235
  ensureSession,
176
236
  }
177
237
  },
@@ -47,6 +47,35 @@ function AssistantBubble(props: { msg: ApxMessage }) {
47
47
  )
48
48
  }
49
49
 
50
+ function ShellBubble(props: { msg: ApxMessage }) {
51
+ const { theme } = useTheme()
52
+ const header = () => {
53
+ const code = props.msg.exitCode
54
+ const status = props.msg.streaming
55
+ ? "running"
56
+ : code === 0
57
+ ? "exit 0"
58
+ : code == null
59
+ ? "ended"
60
+ : `exit ${code}`
61
+ return `$ ${props.msg.command ?? ""} · ${status}`
62
+ }
63
+ const body = () => props.msg.text || (props.msg.streaming ? "…" : "(no output)")
64
+ return (
65
+ <box flexDirection="column" marginBottom={1} paddingLeft={2} paddingRight={2}>
66
+ <text color={theme.warning ?? theme.primary} bold>
67
+ {header()}
68
+ </text>
69
+ <text
70
+ color={props.msg.exitCode && props.msg.exitCode !== 0 ? theme.error : theme.text}
71
+ wrap
72
+ >
73
+ {body()}
74
+ </text>
75
+ </box>
76
+ )
77
+ }
78
+
50
79
  export function Session() {
51
80
  const dims = useTerminalDimensions()
52
81
  const { theme } = useTheme()
@@ -109,7 +138,11 @@ export function Session() {
109
138
  inputEl.clear()
110
139
  setSending(true)
111
140
  try {
112
- await sync.sendMessage(text)
141
+ if (text.startsWith("!") && text.length > 1) {
142
+ await sync.runShell(text.slice(1).trim())
143
+ } else {
144
+ await sync.sendMessage(text)
145
+ }
113
146
  } catch (e) {
114
147
  toast.error(e instanceof Error ? e : new Error(String(e)))
115
148
  } finally {
@@ -132,17 +165,17 @@ export function Session() {
132
165
  fallback={
133
166
  <box paddingLeft={2} paddingTop={2}>
134
167
  <text color={theme.textMuted} italic>
135
- Type a message and press Enter to start chatting.
168
+ Type a message to chat, or prefix with ! to run a shell command (e.g. !ls).
136
169
  </text>
137
170
  </box>
138
171
  }
139
172
  >
140
173
  <For each={messages()}>
141
- {(msg) => (
142
- <Show when={msg.role === "user"} fallback={<AssistantBubble msg={msg} />}>
143
- <UserBubble msg={msg} />
144
- </Show>
145
- )}
174
+ {(msg) => {
175
+ if (msg.role === "user") return <UserBubble msg={msg} />
176
+ if (msg.role === "shell") return <ShellBubble msg={msg} />
177
+ return <AssistantBubble msg={msg} />
178
+ }}
146
179
  </For>
147
180
  </Show>
148
181
  <box height={1} />
@@ -163,7 +196,7 @@ export function Session() {
163
196
  inputEl = r
164
197
  promptRef.set(makeRef(r))
165
198
  }}
166
- placeholder={sending() ? "Waiting for response…" : "Ask anything... (Enter to send)"}
199
+ placeholder={sending() ? "Waiting for response…" : "Ask anything... (prefix ! to run shell, e.g. !ls)"}
167
200
  placeholderColor={theme.textMuted}
168
201
  textColor={theme.text}
169
202
  focusedTextColor={theme.text}