@dmsdc-ai/aigentry-telepty 0.1.98 → 0.3.5

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.
Files changed (44) hide show
  1. package/AGENTS.md +23 -0
  2. package/CHANGELOG.md +436 -0
  3. package/CLAUDE.md +5 -1
  4. package/README.md +70 -1
  5. package/cli.js +232 -53
  6. package/cross-machine.js +132 -0
  7. package/daemon.js +399 -39
  8. package/docs/reports/2026-05-05-issue-8-claude-review.md +194 -0
  9. package/docs/specs/2026-05-05-issue-8-telepty-init.md +477 -0
  10. package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +447 -0
  11. package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +571 -0
  12. package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +608 -0
  13. package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +139 -0
  14. package/host-spec.js +60 -0
  15. package/mcp-server/index.mjs +24 -3
  16. package/package.json +6 -5
  17. package/scripts/regen-snippet-fixtures.js +42 -0
  18. package/skill-installer.js +42 -6
  19. package/skills/telepty/SKILL.md +1 -1
  20. package/skills/telepty-allow/SKILL.md +1 -1
  21. package/skills/telepty-attach/SKILL.md +1 -1
  22. package/skills/telepty-broadcast/SKILL.md +1 -1
  23. package/skills/telepty-daemon/SKILL.md +1 -1
  24. package/skills/telepty-inject/SKILL.md +76 -4
  25. package/skills/telepty-list/SKILL.md +1 -1
  26. package/skills/telepty-listen/SKILL.md +1 -1
  27. package/skills/telepty-rename/SKILL.md +1 -1
  28. package/skills/telepty-session/SKILL.md +1 -1
  29. package/specs/enforce-report-spec.md +237 -0
  30. package/src/init/print-snippet.js +114 -0
  31. package/src/init/snippets/agents.md +15 -0
  32. package/src/init/snippets/claude.md +15 -0
  33. package/src/init/snippets/gemini.md +15 -0
  34. package/src/prompt-symbol-registry.js +97 -0
  35. package/src/report-enforcement.js +86 -0
  36. package/src/submit-gate.js +269 -0
  37. package/tests/snippet-protocol/v1/golden-agents.json +1 -0
  38. package/tests/snippet-protocol/v1/golden-agents.md +17 -0
  39. package/tests/snippet-protocol/v1/golden-all.json +3 -0
  40. package/tests/snippet-protocol/v1/golden-all.md +53 -0
  41. package/tests/snippet-protocol/v1/golden-claude.json +1 -0
  42. package/tests/snippet-protocol/v1/golden-claude.md +17 -0
  43. package/tests/snippet-protocol/v1/golden-gemini.json +1 -0
  44. package/tests/snippet-protocol/v1/golden-gemini.md +17 -0
package/cross-machine.js CHANGED
@@ -5,10 +5,16 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
  const { getSharedContextPromptPath } = require('./shared-context');
8
+ const { parseHostSpec } = require('./host-spec');
8
9
 
9
10
  const PEERS_PATH = path.join(os.homedir(), '.telepty', 'peers.json');
10
11
  const CONTROL_DIR = path.join(os.homedir(), '.telepty', 'ssh');
11
12
 
13
+ function getPeerTransport(entry) {
14
+ if (!entry) return null;
15
+ return entry.transport || 'ssh';
16
+ }
17
+
12
18
  // SSH ControlMaster socket path pattern
13
19
  function controlPath(target) {
14
20
  return path.join(CONTROL_DIR, `ctrl-${target.replace(/[^a-zA-Z0-9@.-]/g, '_')}`);
@@ -307,6 +313,126 @@ function removePeer(name) {
307
313
  return { success: true };
308
314
  }
309
315
 
316
+ // ── HTTP peer support (no SSH required) ─────────────────────────────────────
317
+ // connect-http records a remote daemon's host:port in peers.json with
318
+ // transport='http'. Subsequent inject/list calls discover sessions via the
319
+ // remote daemon's HTTP API directly. Built for laptop daemons where running
320
+ // sshd is not viable. See GitHub issue #13.
321
+
322
+ async function connectHttp(target, options = {}) {
323
+ const spec = parseHostSpec(target);
324
+ if (!spec.host) {
325
+ return { success: false, error: 'connect-http requires a host (got empty value).' };
326
+ }
327
+
328
+ const name = options.name || spec.host.split('.')[0] || spec.host;
329
+
330
+ const headers = {};
331
+ if (options.token) headers['x-telepty-token'] = options.token;
332
+
333
+ let machineId = name;
334
+ let healthOk = false;
335
+ try {
336
+ const healthUrl = `http://${spec.host}:${spec.port}/api/health`;
337
+ const res = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
338
+ if (!res.ok) {
339
+ return { success: false, error: `Daemon at ${spec.host}:${spec.port} returned HTTP ${res.status} on /api/health.` };
340
+ }
341
+ healthOk = true;
342
+ } catch (err) {
343
+ return { success: false, error: `Cannot reach daemon at ${spec.host}:${spec.port}: ${err.message}` };
344
+ }
345
+
346
+ try {
347
+ const metaUrl = `http://${spec.host}:${spec.port}/api/meta`;
348
+ const res = await fetch(metaUrl, { signal: AbortSignal.timeout(3000), headers });
349
+ if (res.ok) {
350
+ const meta = await res.json();
351
+ if (meta && typeof meta.machine_id === 'string' && meta.machine_id) {
352
+ machineId = meta.machine_id;
353
+ } else if (meta && typeof meta.host === 'string' && meta.host) {
354
+ machineId = meta.host;
355
+ }
356
+ }
357
+ } catch {
358
+ // /api/meta is auth-gated; failure is not fatal — health passed.
359
+ }
360
+
361
+ const peers = loadPeers();
362
+ peers.peers[name] = {
363
+ transport: 'http',
364
+ host: spec.host,
365
+ port: spec.port,
366
+ target: `${spec.host}:${spec.port}`,
367
+ machineId,
368
+ lastConnected: new Date().toISOString()
369
+ };
370
+ if (options.token) {
371
+ peers.peers[name].token = options.token;
372
+ }
373
+ savePeers(peers);
374
+
375
+ return {
376
+ success: true,
377
+ name,
378
+ host: spec.host,
379
+ port: spec.port,
380
+ machineId,
381
+ healthOk
382
+ };
383
+ }
384
+
385
+ function listHttpPeers() {
386
+ const peers = loadPeers().peers || {};
387
+ return Object.entries(peers)
388
+ .filter(([, entry]) => getPeerTransport(entry) === 'http')
389
+ .map(([name, entry]) => ({
390
+ name,
391
+ host: entry.host,
392
+ port: entry.port,
393
+ machineId: entry.machineId,
394
+ lastConnected: entry.lastConnected,
395
+ hasToken: Boolean(entry.token)
396
+ }));
397
+ }
398
+
399
+ async function listHttpRemoteSessions(name, options = {}) {
400
+ const peers = loadPeers().peers || {};
401
+ const entry = peers[name];
402
+ if (!entry || getPeerTransport(entry) !== 'http') return [];
403
+
404
+ const headers = {};
405
+ const token = options.token || entry.token;
406
+ if (token) headers['x-telepty-token'] = token;
407
+
408
+ try {
409
+ const url = `http://${entry.host}:${entry.port}/api/sessions`;
410
+ const res = await fetch(url, {
411
+ signal: AbortSignal.timeout(options.timeoutMs || 3000),
412
+ headers
413
+ });
414
+ if (!res.ok) return [];
415
+ const sessions = await res.json();
416
+ if (!Array.isArray(sessions)) return [];
417
+ return sessions.map((s) => ({
418
+ ...s,
419
+ host: `${entry.host}:${entry.port}`,
420
+ peerName: name,
421
+ peerPort: entry.port
422
+ }));
423
+ } catch {
424
+ return [];
425
+ }
426
+ }
427
+
428
+ async function discoverHttpRemoteSessions(options = {}) {
429
+ const peers = listHttpPeers();
430
+ const results = await Promise.all(
431
+ peers.map((peer) => listHttpRemoteSessions(peer.name, options))
432
+ );
433
+ return results.flat();
434
+ }
435
+
310
436
  module.exports = {
311
437
  connect,
312
438
  disconnect,
@@ -323,5 +449,11 @@ module.exports = {
323
449
  remoteEnsureSharedContext,
324
450
  remoteAttach,
325
451
  findSessionPeer,
452
+ // HTTP peer transport (no SSH required)
453
+ connectHttp,
454
+ listHttpPeers,
455
+ listHttpRemoteSessions,
456
+ discoverHttpRemoteSessions,
457
+ getPeerTransport,
326
458
  PEERS_PATH
327
459
  };