@surph_ai/sdk 0.0.25 → 0.0.27

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.
@@ -44931,7 +44931,7 @@ var require_searchContextItems = __commonJS({
44931
44931
  const controller = new AbortController();
44932
44932
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
44933
44933
  const params = { query: query.trim(), context: contextSlug, limit: cappedLimit };
44934
- console.log("SEARCH CONTEXT: ", JSON.stringify(params, null, 2));
44934
+ console.log("[searchContextItems] SEARCH CONTEXT: ", JSON.stringify(params, null, 2));
44935
44935
  try {
44936
44936
  const res = await fetch(`${SURPH_RPC_URL}/tools/search`, {
44937
44937
  method: "POST",
@@ -45108,7 +45108,7 @@ var require_searchContext = __commonJS({
45108
45108
  };
45109
45109
  var fetchCollection = async ({ query, contextSlug, collection, limit, signal }) => {
45110
45110
  const params = { query, context: contextSlug, collection, limit };
45111
- console.log("SEARCH CONTEXT: ", JSON.stringify(params, null, 2));
45111
+ console.log("[fetchCollection] SEARCH CONTEXT: ", JSON.stringify(params, null, 2));
45112
45112
  const res = await fetch(`${SURPH_RPC_URL}/tools/search`, {
45113
45113
  method: "POST",
45114
45114
  headers: { "Content-Type": "application/json" },
@@ -63359,6 +63359,7 @@ var require_queryRewriter = __commonJS({
63359
63359
  var LABEL = "[queryRewriter]";
63360
63360
  var HISTORY_TURNS = 4;
63361
63361
  var SKIP_TOKEN = "SKIP";
63362
+ var DELEGATE_TOKEN = "DELEGATE";
63362
63363
  var MAX_KEYWORD_TOKENS = 128;
63363
63364
  var PREFETCH_LIMIT = 20;
63364
63365
  var MAX_ANGLES = 3;
@@ -63371,20 +63372,30 @@ var require_queryRewriter = __commonJS({
63371
63372
  return `${speaker}: ${text}`;
63372
63373
  }).join("\n\n");
63373
63374
  }
63374
- function buildSystemPrompt(contextMeta) {
63375
+ function buildSystemPrompt(contextMeta, customTools = []) {
63375
63376
  const name = contextMeta.name || contextMeta.slug;
63376
63377
  const topics = Array.isArray(contextMeta.topics) ? contextMeta.topics : [];
63377
63378
  const topicsBlock = topics.length ? topics.map((t) => `- ${t?.name || "Untitled"}${t?.description ? ` \u2014 ${t.description}` : ""}`).join("\n") : "(no topics defined)";
63379
+ const hasCustomTools = Array.isArray(customTools) && customTools.length > 0;
63380
+ const customToolsBlock = hasCustomTools ? `
63381
+
63382
+ The user also has these custom tools installed. Each one operates on this same context but returns its own curated/structured results:
63383
+
63384
+ ${customTools.map((t) => `- ${t.name}: ${t.description}`).join("\n")}
63385
+ ` : "";
63386
+ const delegateOption = hasCustomTools ? `
63387
+ - ${DELEGATE_TOKEN} \u2014 one of the custom tools above is clearly what the user is asking for (its description covers the turn). Skip prefetch so the main LLM invokes the tool directly. When in doubt, still emit keywords \u2014 the tool remains available for the main LLM to call.` : "";
63388
+ const delegateInOutputRule = hasCustomTools ? `${DELEGATE_TOKEN}, ` : "";
63378
63389
  return `You are a query rewriter for a retrieval pipeline. A user is chatting inside a context named "${name}" (slug: ${contextMeta.slug}). The context indexes items across these topics:
63379
63390
 
63380
- ${topicsBlock}
63391
+ ${topicsBlock}${customToolsBlock}
63381
63392
 
63382
63393
  For the latest user turn, decide whether the assistant needs to retrieve items from this context to answer it. Then output ONE of:
63383
63394
 
63384
- - ${SKIP_TOKEN} \u2014 the latest turn does NOT need retrieval. Use this ONLY for: greetings, thanks, meta/small-talk, questions about the assistant itself ("who are you", "what can you do"), or pure clarification questions about the wording of the assistant's prior reply ("what did you mean by X", "can you rephrase point 3"). Anything unrelated to the topics above also SKIPs.
63395
+ - ${SKIP_TOKEN} \u2014 the latest turn does NOT need retrieval. Use this ONLY for: greetings, thanks, meta/small-talk, questions about the assistant itself ("who are you", "what can you do"), or pure clarification questions about the wording of the assistant's prior reply ("what did you mean by X", "can you rephrase point 3"). Anything unrelated to the topics above also SKIPs.${delegateOption}
63385
63396
  - 1 to ${MAX_ANGLES} keyword lines, one per line. Each line is a DISTINCT ANGLE on the same underlying question \u2014 different vocabulary, different specificity, different framings someone else might use for the same concept. The search index is keyword-based and narrow: multiple angles improve recall.
63386
63397
 
63387
- Each line: 3 to 10 keywords or short phrases separated by spaces. No punctuation, no numbering, no bullets, no quotes, no explanations.
63398
+ Each keyword line: 3 to 10 keywords or short phrases separated by spaces. No punctuation, no numbering, no bullets, no quotes, no explanations.
63388
63399
 
63389
63400
  Example \u2014 user asks "what orgs help me raise investment?":
63390
63401
  startup investors venture capital funding
@@ -63397,7 +63408,7 @@ Rules:
63397
63408
  - If the question is narrow enough that one angle covers it (e.g. a specific named entity), one line is fine.
63398
63409
  - Short follow-ups that ask for a new facet of a prior subject (who/when/where/how/how much, "any others", "more like this") are CONTENT lookups, not meta chatter. Resolve the pronoun/topic from the conversation history and combine (a) the subject entity from the prior turn with (b) the new facet the user is asking about. Do NOT skip these.
63399
63410
  - Focus keywords on nouns, entities, and domain terms. Drop filler words. If the user's turn is a pronoun-only follow-up, the keywords MUST include the subject noun from the prior turn.
63400
- - Output ONLY ${SKIP_TOKEN} or the keyword lines. Nothing else.`;
63411
+ - Output ONLY ${SKIP_TOKEN}, ${delegateInOutputRule}or the keyword lines. Nothing else.`;
63401
63412
  }
63402
63413
  async function runCheapModel({ provider, cheapModel, systemPrompt, userMessage }) {
63403
63414
  if (provider === "openai") {
@@ -63453,12 +63464,14 @@ Rules:
63453
63464
  if (!cleaned) return { skip: true };
63454
63465
  const lines = cleaned.split("\n").map((s) => s.trim()).filter(Boolean);
63455
63466
  if (lines.length === 0) return { skip: true };
63456
- if (lines[0].toUpperCase() === SKIP_TOKEN) return { skip: true };
63457
- const angles = lines.map((l) => l.replace(/^[-*•]+\s*|^\d+[.)]\s*/, "").replace(/^["'`]+|["'`]+$/g, "").trim()).filter((l) => l && l.toUpperCase() !== SKIP_TOKEN).slice(0, MAX_ANGLES);
63467
+ const first = lines[0].toUpperCase();
63468
+ if (first === SKIP_TOKEN) return { skip: true };
63469
+ if (first === DELEGATE_TOKEN) return { delegate: true };
63470
+ const angles = lines.map((l) => l.replace(/^[-*•]+\s*|^\d+[.)]\s*/, "").replace(/^["'`]+|["'`]+$/g, "").trim()).filter((l) => l && l.toUpperCase() !== SKIP_TOKEN && l.toUpperCase() !== DELEGATE_TOKEN).slice(0, MAX_ANGLES);
63458
63471
  if (angles.length === 0) return { skip: true };
63459
63472
  return { skip: false, angles };
63460
63473
  }
63461
- async function rewriteAndSearch({ messages, contextMeta, model, session, outputStream = null }) {
63474
+ async function rewriteAndSearch({ messages, contextMeta, model, session, outputStream = null, customTools = [] }) {
63462
63475
  if (!contextMeta?.slug || !Array.isArray(messages) || messages.length === 0) {
63463
63476
  return { skipped: true, reason: "no-context-or-messages" };
63464
63477
  }
@@ -63468,12 +63481,15 @@ Rules:
63468
63481
  console.warn(`${LABEL} no cheap model for provider ${provider}; skipping rewrite`);
63469
63482
  return { skipped: true, reason: "no-cheap-model" };
63470
63483
  }
63471
- const systemPrompt = buildSystemPrompt(contextMeta);
63484
+ const usableTools = Array.isArray(customTools) ? customTools.filter((t) => t && typeof t.name === "string" && t.name.trim() && typeof t.description === "string" && t.description.trim()) : [];
63485
+ const hasCustomTools = usableTools.length > 0;
63486
+ const systemPrompt = buildSystemPrompt(contextMeta, usableTools);
63487
+ const outputChoices = hasCustomTools ? `${SKIP_TOKEN}, ${DELEGATE_TOKEN}, or 1-${MAX_ANGLES} keyword lines` : `${SKIP_TOKEN} or 1-${MAX_ANGLES} keyword lines`;
63472
63488
  const userMessage = `Conversation so far:
63473
63489
 
63474
63490
  ${formatConversation(messages)}
63475
63491
 
63476
- Output ${SKIP_TOKEN} or 1-${MAX_ANGLES} keyword lines (one per line).`;
63492
+ Output ${outputChoices} (one per line).`;
63477
63493
  let raw = "";
63478
63494
  try {
63479
63495
  raw = await runCheapModel({ provider, cheapModel, systemPrompt, userMessage });
@@ -63486,6 +63502,14 @@ Output ${SKIP_TOKEN} or 1-${MAX_ANGLES} keyword lines (one per line).`;
63486
63502
  console.log(`${LABEL} SKIP raw="${raw.trim().slice(0, 80)}" context=${contextMeta.slug}`);
63487
63503
  return { skipped: true, reason: "model-skip" };
63488
63504
  }
63505
+ if (parsed.delegate) {
63506
+ if (hasCustomTools) {
63507
+ console.log(`${LABEL} DELEGATE to custom tool context=${contextMeta.slug} tools=${usableTools.map((t) => t.name).join(",")}`);
63508
+ return { skipped: true, reason: "delegate-to-custom-tool" };
63509
+ }
63510
+ console.log(`${LABEL} DELEGATE received but no custom tools installed \u2014 treating as SKIP context=${contextMeta.slug}`);
63511
+ return { skipped: true, reason: "delegate-without-tools" };
63512
+ }
63489
63513
  const displayKeywords = parsed.angles.join(" | ");
63490
63514
  console.log(`${LABEL} angles=${parsed.angles.length} keywords="${displayKeywords}" context=${contextMeta.slug}`);
63491
63515
  const searches = await Promise.all(
@@ -94448,6 +94472,17 @@ Rules:
94448
94472
  === Retrieved items (${items.length}) ===
94449
94473
  ${lines}
94450
94474
  === End retrieved items ===`;
94475
+ }
94476
+ function frameCustomToolRouting(customTools, contextMeta) {
94477
+ if (!contextMeta?.slug) return null;
94478
+ const usable = Array.isArray(customTools) ? customTools.filter((t) => t && typeof t.name === "string" && t.name.trim() && typeof t.description === "string" && t.description.trim()) : [];
94479
+ if (usable.length === 0) return null;
94480
+ const toolList = usable.map((t) => `- ${t.name}: ${t.description}`).join("\n");
94481
+ return `The user has these custom tools installed. Each is purpose-built for a specific type of query about the active context:
94482
+
94483
+ ${toolList}
94484
+
94485
+ Routing rule: when the user's turn matches one of these tools' descriptions, PREFER the custom tool over \`searchContext\`. The custom tool returns curated/structured results tailored to that query type; \`searchContext\` is a generic keyword search. Only fall back to \`searchContext\` for questions the custom tools don't cover.`;
94451
94486
  }
94452
94487
  function frameDate() {
94453
94488
  const now = /* @__PURE__ */ new Date();
@@ -94616,15 +94651,18 @@ Always use absolute paths for file operations. When the user asks to create a fi
94616
94651
  workingDirectory,
94617
94652
  systemFromHistory,
94618
94653
  contextMeta = null,
94619
- prefetch = null
94654
+ prefetch = null,
94655
+ customTools = []
94620
94656
  }) {
94621
94657
  const tideBlock = frameTide(params?.tide);
94622
94658
  const contextFramed = contextMeta ? frameContext(contextMeta) : null;
94659
+ const customRoutingFramed = frameCustomToolRouting(customTools, contextMeta);
94623
94660
  const prefetchFramed = prefetch?.items?.length ? framePrefetchedItems({ contextMeta, items: prefetch.items, keywords: prefetch.keywords }) : null;
94624
94661
  const dateBlock = frameDate();
94625
94662
  const blocks = [];
94626
94663
  if (provider === "anthropic") {
94627
94664
  if (contextFramed) blocks.push({ text: contextFramed, cacheable: true });
94665
+ if (customRoutingFramed) blocks.push({ text: customRoutingFramed, cacheable: false });
94628
94666
  if (prefetchFramed) blocks.push({ text: prefetchFramed, cacheable: false });
94629
94667
  blocks.push({ text: dateBlock, cacheable: false });
94630
94668
  if (systemFromHistory) blocks.push({ text: systemFromHistory, cacheable: false });
@@ -94633,6 +94671,7 @@ Always use absolute paths for file operations. When the user asks to create a fi
94633
94671
  }
94634
94672
  if (tideBlock) blocks.push({ text: tideBlock, cacheable: false });
94635
94673
  if (contextFramed) blocks.push({ text: contextFramed, cacheable: true });
94674
+ if (customRoutingFramed) blocks.push({ text: customRoutingFramed, cacheable: false });
94636
94675
  if (prefetchFramed) blocks.push({ text: prefetchFramed, cacheable: false });
94637
94676
  blocks.push({ text: dateBlock, cacheable: false });
94638
94677
  const workingDirInstr = buildWorkingDirectoryInstruction(workingDirectory);
@@ -94718,9 +94757,10 @@ Always use absolute paths for file operations. When the user asks to create a fi
94718
94757
  }
94719
94758
  const activeContextSlug = params?.context?.slug || params?.currentContextSlug || null;
94720
94759
  const contextMeta = activeContextSlug ? await fetchContext({ slug: activeContextSlug }) : null;
94760
+ const customTools = Array.isArray(params?.session?.user?.tools) ? params.session.user.tools : [];
94721
94761
  let prefetch = null;
94722
94762
  if (params?.context?.slug && contextMeta) {
94723
- const rw = await rewriteAndSearch({ messages, contextMeta, model, session, outputStream });
94763
+ const rw = await rewriteAndSearch({ messages, contextMeta, model, session, outputStream, customTools });
94724
94764
  if (!rw.skipped && Array.isArray(rw.items) && rw.items.length > 0) {
94725
94765
  prefetch = { items: rw.items, keywords: rw.keywords };
94726
94766
  }
@@ -94734,7 +94774,8 @@ Always use absolute paths for file operations. When the user asks to create a fi
94734
94774
  workingDirectory,
94735
94775
  systemFromHistory,
94736
94776
  contextMeta,
94737
- prefetch
94777
+ prefetch,
94778
+ customTools
94738
94779
  });
94739
94780
  console.log(`
94740
94781
 
@@ -96,8 +96,8 @@ router.post('/custom', async (req, res, next) => {
96
96
 
97
97
  const tools = await loadTool()
98
98
  const response = await handler({ args, context, user }, tools)
99
- if (resp.error) {
100
- throw new Error(resp.error);
99
+ if (response.error) {
100
+ throw new Error(response.error);
101
101
  }
102
102
 
103
103
  res.json({ response })
package/types.d.ts CHANGED
@@ -61,3 +61,12 @@ export type ToolHandler<
61
61
  TArgs = Record<string, unknown>,
62
62
  TResult = unknown,
63
63
  > = (input: ToolHandlerInput<TArgs>) => Promise<TResult> | TResult;
64
+
65
+ export interface Surph {
66
+ fetchUser(username: string): Promise<User>;
67
+ fetchContext(slug: string | ToolContext): Promise<unknown>;
68
+ searchContext(ctxSlug: string, query: string): Promise<unknown>;
69
+ search(query: string, ctxSlug?: string | null): Promise<unknown>;
70
+ }
71
+
72
+ export declare const surph: Surph;
package/utils/index.js CHANGED
File without changes
package/auth/index.js DELETED
@@ -1,147 +0,0 @@
1
- const prompt = require('prompt')
2
- const colors = require('colors/safe')
3
- const path = require('path')
4
- const os = require('os')
5
- const fs = require('fs')
6
- const utils = require('../utils')
7
-
8
- module.exports = {
9
- showPrompt: () => {
10
- return new Promise((resolve, reject) => {
11
- const schema = {
12
- properties: {
13
- email: {
14
- description: colors.cyan('\nEmail'),
15
- required: true
16
- },
17
- password: {
18
- description: colors.cyan('Password'),
19
- required: true,
20
- hidden: true
21
- }
22
- }
23
- }
24
-
25
- prompt.message = null
26
- prompt.start()
27
-
28
- // Get two properties from the user: username and email
29
- prompt.get(schema, (err, result) => {
30
- if (err) {
31
- reject(err)
32
- return
33
- }
34
-
35
- resolve(result)
36
- })
37
- })
38
- },
39
-
40
- isLoggedIn: () => {
41
- const userProfilePath = path.join(os.homedir(), '.surph_user')
42
- if (!fs.existsSync(userProfilePath)) {
43
- return false
44
- }
45
-
46
- return true
47
- },
48
-
49
- connectApp: () => {
50
- // https://www.npmjs.com/package/prompt
51
- return new Promise((resolve, reject) => {
52
- const schema = {
53
- properties: {
54
- siteId: {
55
- description: colors.cyan('\nEnter Site ID'),
56
- required: true
57
- },
58
- apiKey: {
59
- description: colors.cyan('Enter Site API Key'),
60
- required: true
61
- }
62
- }
63
- }
64
-
65
- prompt.message = null
66
-
67
- prompt.start()
68
- prompt.get(schema, (err, result) => {
69
- if (err){
70
- reject(err)
71
- return
72
- }
73
-
74
- resolve(result)
75
- })
76
- })
77
- },
78
-
79
- currentUser: () => {
80
- return new Promise((resolve, reject) => {
81
- const userProfilePath = path.join(os.homedir(), '.surph_user')
82
- if (!fs.existsSync(userProfilePath)) {
83
- // Not logged in
84
- resolve(false);
85
- return
86
- }
87
-
88
- try {
89
- const data = utils.readFile(userProfilePath)
90
- const currentUser = JSON.parse(data)
91
- resolve(currentUser)
92
- } catch (error) {
93
- reject(error)
94
- }
95
- })
96
- },
97
-
98
- isAuthorized: (app, currentUser) => {
99
- if (!currentUser)
100
- return false
101
-
102
- if (app.profile.id === currentUser.id) // user is admin
103
- return true
104
-
105
- // check if currentUser is a collaborator:
106
- var isCollaborator = false
107
- for (var i=0; i<app.collaborators.length; i++){
108
- var collaborator = app.collaborators[i]
109
- if (collaborator.id == currentUser.id){
110
- isCollaborator = true
111
- break
112
- }
113
- }
114
-
115
- return isCollaborator
116
- },
117
-
118
- /*
119
- awsConfig: function(){
120
- return new Promise(function(resolve, reject){
121
- var awsConfigPath = path.join(os.homedir(), '.turbo_aws_config')
122
- if (fs.existsSync(awsConfigPath) == false) {
123
- reject(new Error('AWS Config not set. To set:\n$ turbo awsConfig'))
124
- return
125
- }
126
-
127
- utils.readFile(awsConfigPath)
128
- .then(function(data){
129
- awsSettings = JSON.parse(data)
130
- resolve(awsSettings)
131
- return
132
- })
133
- .catch(function(err){
134
- reject(err)
135
- })
136
- })
137
- }
138
-
139
- awsConfigSet: function(){
140
- var awsConfigPath = path.join(os.homedir(), '.turbo_aws_config')
141
- if (fs.existsSync(awsConfigPath) == false) {
142
- return false
143
- }
144
-
145
- return true
146
- } */
147
- }