@openagents-org/agent-launcher 0.2.160 → 0.2.162

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.
@@ -869,6 +869,26 @@ function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoi
869
869
  return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
870
870
  }
871
871
 
872
+ /**
873
+ * Build the system prompt appended to Pi's own prompt (`--append-system-prompt`).
874
+ *
875
+ * Shape mirrors buildOpenCodeSystemPrompt: identity + browser directive +
876
+ * collaboration + mode + the REST "skills" block, because Pi is NOT wired to an
877
+ * MCP server (only the Claude adapter uses --mcp-config). Pi reaches workspace
878
+ * APIs through its built-in `bash` tool + curl, exactly like OpenCode/Amp.
879
+ *
880
+ * NOTE: this is APPENDED, never passed via `--system-prompt` — that flag would
881
+ * REPLACE Pi's own coding-assistant prompt and strip its tool instructions.
882
+ */
883
+ function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
884
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
885
+ const directive = buildBrowserDirective(browserEnabled);
886
+ const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
887
+ const modePrompt = buildModePrompt(mode);
888
+ const api = buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode });
889
+ return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
890
+ }
891
+
872
892
  /**
873
893
  * Build workspace skill markdown for OpenCode (written to .opencode/skills/).
874
894
  */
@@ -972,6 +992,7 @@ module.exports = {
972
992
  buildOpenclawSkillMd,
973
993
  buildOpenCodeSystemPrompt,
974
994
  buildOpenCodeSkillMd,
995
+ buildPiSystemPrompt,
975
996
  buildClaudeSkillMd,
976
997
  buildCursorSkillMd,
977
998
  };
package/src/daemon.js CHANGED
@@ -51,7 +51,7 @@ class Daemon {
51
51
  if (!this._nodeClient) {
52
52
  this._nodeClient = new WorkspaceClient(n.endpoint);
53
53
  }
54
- const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster(), runtimes: this._runtimes };
54
+ const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster(), runtimes: this._runtimes, fs: this._buildFs() };
55
55
  const resp = await this._nodeClient.nodeHeartbeat(n.node_id, n.token, info);
56
56
  // The heartbeat response is our push channel: run any queued remote
57
57
  // agent-management commands the workspace enqueued for this node. Fire
@@ -80,7 +80,19 @@ class Daemon {
80
80
  try {
81
81
  for (const a of this.config.getAgents()) {
82
82
  const proc = this._processes[a.name];
83
- roster.push({ name: a.name, type: a.type || 'unknown', status: (proc && proc.state) || 'stopped' });
83
+ // Model: per-agent env override, else the type-level saved model. Working
84
+ // dir: the agent's configured path. Both power the workspace agent cards.
85
+ let model = (a.env && a.env.LLM_MODEL) || null;
86
+ if (!model) {
87
+ try { model = (this.envManager.load(a.type) || {}).LLM_MODEL || null; } catch { model = null; }
88
+ }
89
+ roster.push({
90
+ name: a.name,
91
+ type: a.type || 'unknown',
92
+ status: (proc && proc.state) || 'stopped',
93
+ model: model || null,
94
+ workingDir: a.path || null,
95
+ });
84
96
  }
85
97
  } catch {
86
98
  // best-effort
@@ -88,6 +100,36 @@ class Daemon {
88
100
  return roster;
89
101
  }
90
102
 
103
+ /**
104
+ * Filesystem hint for the working-directory picker: home + its immediate
105
+ * (non-hidden) subfolders, so the workspace can show real folders instantly.
106
+ */
107
+ _buildFs() {
108
+ try {
109
+ const home = os.homedir();
110
+ const dirs = fs.readdirSync(home, { withFileTypes: true })
111
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
112
+ .map((d) => d.name)
113
+ .sort((a, b) => a.localeCompare(b))
114
+ .slice(0, 100);
115
+ return { home, dirs };
116
+ } catch {
117
+ return {};
118
+ }
119
+ }
120
+
121
+ /** List the (non-hidden) subfolders of a directory, for on-demand browsing. */
122
+ _listDir(dir) {
123
+ const target = dir && String(dir).trim() ? String(dir) : os.homedir();
124
+ const dirs = fs.readdirSync(target, { withFileTypes: true })
125
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
126
+ .map((d) => d.name)
127
+ .sort((a, b) => a.localeCompare(b))
128
+ .slice(0, 500);
129
+ const parent = path.dirname(target);
130
+ return { path: target, parent: parent === target ? null : parent, dirs };
131
+ }
132
+
91
133
  /**
92
134
  * Detect, for every supported agent type, whether its runtime is installed and
93
135
  * logged-in/ready on this device. Runs `agn runtimes --json` in a CHILD process
@@ -117,6 +159,7 @@ class Daemon {
117
159
  const name = (args.name || '').trim();
118
160
  let ok = false;
119
161
  let message = '';
162
+ let data = null;
120
163
  try {
121
164
  if (action === 'create_agent') {
122
165
  const type = (args.type || '').trim();
@@ -150,6 +193,29 @@ class Daemon {
150
193
  const r = await this._runAgn(['remove', name]);
151
194
  ok = r.code === 0;
152
195
  message = ok ? `Agent '${name}' removed` : (r.stderr || r.stdout || 'remove failed');
196
+ } else if (action === 'configure_agent') {
197
+ // Reconfigure an existing agent: update model/key (type-level env), then
198
+ // restart it so the change takes effect. Working-dir changes require a
199
+ // recreate (agn has no in-place path change) at the new path.
200
+ const type = (args.type || '').trim();
201
+ if (args.apiKey) await this._runAgn(['env', type, '--set', `LLM_API_KEY=${args.apiKey}`]);
202
+ if (args.model !== undefined) {
203
+ await this._runAgn(['env', type, '--set', `LLM_MODEL=${String(args.model || '')}`]);
204
+ }
205
+ const newDir = (args.workingDir || '').trim();
206
+ if (newDir && newDir !== (args.currentWorkingDir || '')) {
207
+ try { fs.mkdirSync(newDir, { recursive: true }); } catch {}
208
+ await this._runAgn(['remove', name]);
209
+ const rc = await this._runAgn(['create', name, '--type', type, '--install', '--path', newDir]);
210
+ if (rc.code !== 0) throw new Error(rc.stderr || rc.stdout || 'recreate failed');
211
+ const rc2 = await this._runAgn(['connect', name, n.token, '--endpoint', n.endpoint]);
212
+ if (rc2.code !== 0) throw new Error(rc2.stderr || rc2.stdout || 'connect failed');
213
+ } else {
214
+ await this._runAgn(['stop', name]);
215
+ await this._runAgn(['start', name]);
216
+ }
217
+ ok = true;
218
+ message = `Agent '${name}' reconfigured`;
153
219
  } else if (action === 'detect_runtimes') {
154
220
  await this._refreshRuntimes();
155
221
  // Push the fresh detection right away rather than waiting for the next
@@ -157,6 +223,10 @@ class Daemon {
157
223
  this._nodeHeartbeat();
158
224
  ok = true;
159
225
  message = `Detected ${this._runtimes.length} runtime(s)`;
226
+ } else if (action === 'list_dir') {
227
+ data = this._listDir(args.path);
228
+ ok = true;
229
+ message = `Listed ${data.dirs.length} folder(s)`;
160
230
  } else {
161
231
  message = `Unknown action '${action}'`;
162
232
  }
@@ -165,7 +235,7 @@ class Daemon {
165
235
  message = e.message || String(e);
166
236
  }
167
237
  try {
168
- await this._nodeClient.nodeCommandResult(cmd.commandId, n.token, { ok, message });
238
+ await this._nodeClient.nodeCommandResult(cmd.commandId, n.token, { ok, message, data });
169
239
  } catch {
170
240
  // best-effort; the command stays 'running' if we can't report back
171
241
  }
package/src/installer.js CHANGED
@@ -665,7 +665,12 @@ class Installer {
665
665
  const directReady = directEnv || directSaved;
666
666
  const envAnyReady = this._hasAnyValue(process.env, checkReady.env_vars);
667
667
  const savedAnyReady = !!(checkReady.saved_env_key && savedEnv[checkReady.saved_env_key]);
668
- const credsReady = this._checkCredsReady(checkReady);
668
+ // A registry entry may require a non-empty JSON object rather than mere
669
+ // parseability. In that mode the stricter _evaluateCredsFile path below is
670
+ // authoritative; the legacy parser treats an empty {} as ready.
671
+ const credsReady = checkReady.creds_json_has_entries
672
+ ? false
673
+ : this._checkCredsReady(checkReady);
669
674
  // Content-free credential probes (opt-in via check_ready). Detect agents
670
675
  // whose sign-in is a local OAuth/credential FILE (e.g. Gemini's
671
676
  // ~/.gemini/oauth_creds.json) or a service-account file path, WITHOUT
@@ -792,9 +797,12 @@ class Installer {
792
797
  }
793
798
 
794
799
  /**
795
- * Existence/readability of a credential file WITHOUT reading its contents —
796
- * never loads a token/key into memory. Returns 'present' | 'unreadable' |
797
- * 'absent'. A non-empty readable file (or a non-empty directory) is 'present';
800
+ * Existence/readability of a credential file. Returns 'present' |
801
+ * 'unreadable' | 'absent'. A non-empty readable file (or a non-empty
802
+ * directory) is normally 'present'. Entries with creds_json_has_entries
803
+ * additionally require at least one top-level JSON entry, allowing an empty
804
+ * auto-created {} file to remain signed out. Credential values are never
805
+ * logged or returned.
798
806
  * a path that exists but cannot be statted/read is 'unreadable'. Resolves "~"
799
807
  * against the running user's home (the daemon/launcher user), not the
800
808
  * renderer/browser user.
@@ -809,6 +817,14 @@ class Installer {
809
817
  const st = fs.statSync(p);
810
818
  if (st.isDirectory()) return fs.readdirSync(p).length > 0 ? 'present' : 'absent';
811
819
  fs.accessSync(p, fs.constants.R_OK);
820
+ if (checkReady.creds_json_has_entries) {
821
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
822
+ if (Array.isArray(parsed)) return parsed.length > 0 ? 'present' : 'absent';
823
+ if (parsed && typeof parsed === 'object') {
824
+ return Object.keys(parsed).length > 0 ? 'present' : 'absent';
825
+ }
826
+ return 'absent';
827
+ }
812
828
  return st.size > 0 ? 'present' : 'absent';
813
829
  } catch {
814
830
  return 'unreadable';
@@ -143,13 +143,13 @@ class WorkspaceClient {
143
143
  * Report the outcome of a remote node command via
144
144
  * POST /v1/nodes/commands/{id}/result (authenticated by the workspace token).
145
145
  */
146
- async nodeCommandResult(commandId, token, { ok, message } = {}) {
147
- const data = await this._post(
146
+ async nodeCommandResult(commandId, token, { ok, message, data } = {}) {
147
+ const resp = await this._post(
148
148
  `/v1/nodes/commands/${commandId}/result`,
149
- { ok: !!ok, message: message || null },
149
+ { ok: !!ok, message: message || null, data: data ?? null },
150
150
  this._wsHeaders(token),
151
151
  );
152
- return data.data || data;
152
+ return resp.data || resp;
153
153
  }
154
154
 
155
155
  /**